ποΈGitΠ―ΡΠ°ποΈ
Commit 01e5e5192e3fc89a959e4299f46cac55aafb82d2
Parents : d714b22
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-29T13:54:24-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-29T13:54:24-05:00
docs: distributable Obtainium configurations for every release channel (#6515)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Changes
10 files changed, 482 insertions(+), 126 deletions(-)
Diff
diff --git a/README.md b/README.md
index aaf956f783..fa42d53d96 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Highlights from the 2.8.0 release:
## Get Meshtastic
-The easiest and fastest way to get the latest releases is to use our [GitHub releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommended to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates automatically.
+The easiest and fastest way to get the latest releases is to use our [GitHub releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommended to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates automatically β ready-made configurations for every channel (stable, beta, snapshot) are in [`obtainium/`](obtainium/), and [Test Builds & Obtainium](docs/en/developer/test-builds.md) explains the setup.
Alternatively, these other providers are also available, but may be slower to update.
diff --git a/docs/en/developer.md b/docs/en/developer.md
index 7157fc1da5..412db9ef42 100644
--- a/docs/en/developer.md
+++ b/docs/en/developer.md
@@ -34,6 +34,8 @@ Things that trip up first-time contributors β check these before requesting re
Keep the last 5β8 entries and trim older ones from the bottom.
-->
+**July 2026** β [Test Builds & Obtainium](developer/test-builds) β New page, replacing the root `obtainium-test-builds.md`. Distributable Obtainium configurations now live in `obtainium/` (importable export, one-tap link generator, config-site submission).
+
**July 2026** β Map layer stack (`MapLayer.kt`, `MapLayersManager`, GeoJSON/KML import, Site Planner) extracted from the Google flavor into shared `androidApp/src/main` source (#6148) β F-Droid now renders imported overlays via a new OSMdroid-based renderer, so both flavors compile one implementation.
**July 2026** β [Persistence](developer/persistence) β Local Mesh Discovery sessions and cached `msh.to` device links now persist to Room (`DiscoverySessionEntity`, `DiscoveryPresetResultEntity`, `DiscoveredNodeEntity`, `DeviceLinkEntity`).
diff --git a/docs/en/developer/test-builds.md b/docs/en/developer/test-builds.md
new file mode 100644
index 0000000000..21e937c05b
--- /dev/null
+++ b/docs/en/developer/test-builds.md
@@ -0,0 +1,169 @@
+---
+title: Test Builds & Obtainium
+parent: Developer Guide
+nav_order: 10
+last_updated: 2026-07-29
+description: Install and auto-update Meshtastic test builds from GitHub releases with Obtainium β channel configurations, APK selection, and the shareable config format.
+aliases:
+ - test-builds
+ - obtainium
+ - beta
+ - snapshot
+---
+
+# Test Builds & Obtainium
+
+[Obtainium](https://github.com/ImranR98/Obtainium) installs and auto-updates Android apps straight from their GitHub releases β no Play Store account, no testing-track invite. This page sets it up for Meshtastic and shows how to follow the **open beta**, **closed beta**, or **snapshot** channels.
+
+> **Heads up β signatures.** Builds from GitHub are signed with the project's release key (`CN=Kevin Hester, O=Geeksville Industries`), **not** Google Play's. If you already have Meshtastic installed from the Play Store, Android will refuse to update over it: **uninstall the Play Store version first** (this clears app data), then install via Obtainium and stay on Obtainium for updates.
+>
+> **Switching between the `fdroid` and `google` flavors is fine.** Both flavors in a given release carry the *same* release key, so Obtainium can move you from one to the other in place β verified by installing the `google` APK over an installed `fdroid` build. Only the *origin* of a build (Play vs GitHub) determines whether the signatures clash.
+
+---
+
+## Which channels can Obtainium reach?
+
+Meshtastic for Android promotes builds up a ladder: `closed β open β production`. Each maps to a Google Play track, and the GitHub release is published (un-drafted) when a build is promoted to closed or higher.
+
+| Channel | Play track | On GitHub | Obtainium can install it? |
+|---|---|---|---|
+| **stable** | Production | published, marked *Latest* | β
Yes |
+| **open** beta | Beta (Open) | published prerelease, tag `vX.Y.Z-open.N` | β
Yes |
+| **closed** beta | Alpha (Closed) | published prerelease, tag `vX.Y.Z-closed.N` | β
Yes |
+| **snapshot** | β (not on Play) | rolling prerelease, tag `snapshot` | β
Yes |
+
+Each promotion cuts its **own** tag, and older ones stay published β `v2.8.0-open.1` through `v2.8.0-open.6` all exist as installable releases with APKs attached. So a channel accumulates releases during a version cycle rather than moving a single one forward, and the channel configs below pick the **newest** release whose title matches the channel.
+
+**Snapshot** is different: it's an automated debug build of the latest commit on `main`, rebuilt and re-published under the single moving `snapshot` tag on every push. It never goes to Play. Because debug builds use a `.debug` application-ID suffix (`com.geeksville.mesh.fdroid.debug` / `com.geeksville.mesh.google.debug`) and the debug signing key, a snapshot installs as its **own separate app** β it sits alongside a Play/stable/beta install, so the uninstall-first warning above does **not** apply to it.
+
+---
+
+## One-tap setup
+
+Prebuilt configurations live in [`obtainium/`](https://github.com/meshtastic/Meshtastic-Android/tree/main/obtainium) β every setting below is already baked in. Either import [`meshtastic-obtainium-export.json`](https://github.com/meshtastic/Meshtastic-Android/blob/main/obtainium/meshtastic-obtainium-export.json) via **Import/Export β Obtainium import**, or tap a channel link generated by:
+
+```bash
+python3 obtainium/generate-links.py
+```
+
+Every channel except snapshot is the same `com.geeksville.mesh` package, and Obtainium keys apps by package name β so you can follow **one** of stable/open/closed/bleeding-edge at a time, plus snapshot alongside it. Tapping a second channel link switches the existing entry over rather than adding a new one.
+
+To set things up by hand instead, follow the rest of this page.
+
+---
+
+## Manual setup
+
+1. Install Obtainium ([GitHub releases](https://github.com/ImranR98/Obtainium/releases) or [F-Droid / IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)).
+2. Tap **Add App**.
+3. **App Source URL:** `https://github.com/meshtastic/Meshtastic-Android`
+4. Set the options for the channel you want (below).
+5. Tap **Add**, then **Install**.
+
+### Stable
+
+- **Include prereleases:** off
+- *(optional, stricter)* **Verify the 'latest' tag:** on
+- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
+
+### Open beta
+
+- **Include prereleases:** on
+- **Filter release titles by regular expression:** `-open`
+- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
+
+### Closed beta
+
+- **Include prereleases:** on
+- **Filter release titles by regular expression:** `-closed`
+- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
+
+### Bleeding edge (newest promoted test build, any channel)
+
+- **Include prereleases:** on
+- **Filter release titles by regular expression:** `-(closed|open)`
+- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
+
+Obtainium installs the newest promoted prerelease β whatever is currently in open or closed. The title filter is required to skip the always-newer `snapshot` prerelease; without it Obtainium would follow snapshot instead.
+
+### Snapshot (latest commit on `main`)
+
+- **Include prereleases:** on
+- **Filter release titles by regular expression:** `^Snapshot`
+- **Use the latest asset date as the release date:** on
+- **Version detection:** off, with **Release date as version** on
+- **Filter APKs by regular expression:** debug-signed names, see below
+
+Follows `main` directly β updates on every push. These are **debug builds** (`.debug` package, debug key), so they install as a separate app and won't disturb a stable/beta install. The `snapshot` tag never changes, so there is no version string to compare β date-based pseudo-versioning is what makes updates detectable.
+
+The APKs are named `β¦-debug-<versionCode>.apk` (not `-release.apk`), so use debug-suffixed filters:
+
+| You want | Regex |
+|---|---|
+| `google` flavor, most phones (arm64) | `google-arm64-v8a-debug-\d+\.apk` |
+| `fdroid` flavor, most phones (arm64) | `fdroid-arm64-v8a-debug-\d+\.apk` |
+| `fdroid` flavor, one-size-fits-all | `fdroid-universal-debug-\d+\.apk` |
+
+Snapshot releases attach only the debug APKs β no `.aab` or desktop installers.
+
+> **If you build debug locally, uninstall your local build first.** CI signs snapshots with *its own* debug keystore, not yours (`07c16b98β¦` from CI versus `88914c61β¦` from a local machine, for example). Since both produce the same `com.geeksville.mesh.<flavor>.debug` package name, Android refuses the swap with `INSTALL_FAILED_UPDATE_INCOMPATIBLE`. The "installs as a separate app" note above holds against *release* builds, not against your own debug builds.
+
+Two more things about release selection apply to every channel above, not just snapshot:
+
+> **Leave "Fallback to older releases" ON.** It is not a channel-strictness setting, and turning it off breaks every channel config here. Obtainium's release loop breaks out after the first candidate when fallback is off, so a title filter never gets to skip past releases that don't match β a config pinned to `-open` or `^Snapshot` then fails with "Could not find a suitable release" because production `v2.7.14` sits at the head of the list. The title filter is what pins the channel; fallback is what lets Obtainium walk down to the newest release that matches it.
+>
+> **If your channel filter still finds nothing:** that channel genuinely has no published release yet (common early in a version cycle, and always true for `-closed` once builds have moved to `-open`). Use the *Bleeding edge* form above to follow whichever channel is currently live.
+
+---
+
+## Picking the APK
+
+Each release attaches the `google` flavor APK, several `fdroid` flavor APKs, plus an `.aab` (not installable) and the desktop installers. Obtainium ignores non-APK assets on its own, but the two flavors share the `com.geeksville.mesh` package name, so exactly one must be pinned with **Filter APKs by regular expression**:
+
+| You want | Regex |
+|---|---|
+| **Recommended for testing β `google` flavor** | `google-release\.apk` |
+| `fdroid` flavor, most phones (arm64) | `fdroid-arm64-v8a-release\.apk` |
+| `fdroid` flavor, one-size-fits-all | `fdroid-universal-release\.apk` |
+| `fdroid` flavor, let Obtainium pick the ABI | `fdroid-.*-release\.apk` with **Auto-select by architecture** on |
+
+**Use the `google` flavor while testing.** It ships Firebase Crashlytics and Datadog RUM, so the crashes and errors you hit get reported back to the team β which is the whole point of a test phase. It also has Google push (FCM) and Google Maps. Pick an `fdroid` flavor only if you'd rather not send that telemetry. You can change your mind later: both flavors share the release key, so Obtainium updates across the flavor boundary without an uninstall. Expect the `google` flavor to show its analytics-consent screen the first time it starts.
+
+---
+
+## Version detection turns itself off β that's expected
+
+Release tags read `vX.Y.Z`, while our installed `versionName` reads `X.Y.Z (versionCode) <flavor>`. Obtainium tries to reconcile the two against a list of standard version formats, all of which are **fully anchored** β none of them tolerates the spaces and parentheses in our `versionName`. So on the first update check Obtainium logs `Could not reconcile version formats for: com.geeksville.mesh`, sets **Version detection** to off for the app, and records the release tag as the installed version.
+
+**This is harmless and you should not try to fix it.** With detection off, Obtainium compares the release tag it last installed against the newest tag, so updates are still detected and installed normally. What you lose is only the ability to notice a version installed *outside* Obtainium.
+
+In particular, **do not set "Trim the version string"** to `\d+\.\d+\.\d+` hoping to fix it. That setting only rewrites the version parsed from the *release*; the unparseable side is the installed `versionName`, which it never touches β so reconciliation fails exactly as before. It also adds a failure mode: if the regex ever matches nothing (a tag without a three-part version), the update check fails outright with a "no version found" error instead of degrading quietly.
+
+Snapshot is a separate case: its tag is the literal string `snapshot` and never changes, so tag comparison can't work either. Those configs use date-based pseudo-versioning (**Release date as version** plus **Use the latest asset date as the release date**) instead.
+
+---
+
+## The config format
+
+An Obtainium app configuration is a flat JSON object. Four keys are required β `id` (Android package name), `url` (the GitHub repo, which is what selects the source), `author`, and `name`. Everything else goes in `additionalSettings`, a **JSON string nested inside the JSON**, so its quotes and backslashes are escaped one extra level. Any key you omit takes its default.
+
+The same object travels in three shapes:
+
+| Shape | Format | Used for |
+|---|---|---|
+| Import/export file | `{"apps":[β¦],"settings":null}` | **Import/Export β Obtainium import** |
+| Deep link | `obtainium://app/<percent-encoded JSON>` | one-tap add with settings baked in |
+| Config-site entry | `{"configs":[β¦],"icon","categories","description"}` | PRs to [apps.obtainium.imranr.dev](https://apps.obtainium.imranr.dev) |
+
+`obtainium://add/<url>` only prefills the Add-App page; `obtainium://app/β¦` is what carries settings. Wrapping a link as `https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/β¦` keeps it clickable where a custom scheme wouldn't linkify.
+
+Obtainium keys apps by package name, so the two stable flavors cannot coexist as separate entries β the config-site submission expresses them as alternate `configs` with an `altLabel` each. See [`obtainium/README.md`](https://github.com/meshtastic/Meshtastic-Android/blob/main/obtainium/README.md) for the full key reference and the submission checklist.
+
+---
+
+## Notes
+
+- **Play Protect may interrupt the install** with "App scan recommended β Play Protect hasn't seen this app before," offering *Scan app* or *Don't install app*. *Scan app* uploads the APK to Google. Declining cancels the install, so on a device with Play Services you'll need to allow the scan, or install with `adb install` instead. This showed up for a debug-signed snapshot but not for a release-signed build.
+- **Obtainium's first launch shows two full-screen dialogs** (a welcome note and a Play-certification note). They swallow an incoming `obtainium://` link, so dismiss them before using a one-tap link.
+- **Track without installing:** turn on **Track-only** to get update notifications without Obtainium downloading anything.
+- **Snapshot filenames carry the versionCode** so that a moving tag, which reuses the same release and asset URLs forever, still produces a distinguishable asset on every build.
diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt
index 7c3c2a9af4..8197b76a9f 100644
--- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt
+++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt
@@ -588,6 +588,17 @@ class DefaultDocBundleLoader : DocBundleLoader {
5400,
"measurement",
),
+ KeywordIndexEntry(
+ "test-builds",
+ "Test Builds & Obtainium",
+ "developer",
+ "en/developer/test-builds.html",
+ 10,
+ listOf("obtainium", "beta", "snapshot", "apk", "install", "channel", "prerelease"),
+ listOf("test-builds", "obtainium", "beta", "snapshot"),
+ 11600,
+ "test-builds",
+ ),
)
private fun KeywordIndexEntry.toDocPage(): DocPage = DocPage(
diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt
index 87fb9206d1..cc8adcd69e 100644
--- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt
+++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt
@@ -39,6 +39,7 @@ import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.icon.Rssi
import org.meshtastic.core.ui.icon.Settings
import org.meshtastic.core.ui.icon.SignalCellular3Bar
+import org.meshtastic.core.ui.icon.SystemUpdate
import org.meshtastic.core.ui.icon.WavingHand
import org.meshtastic.feature.docs.model.DocPage
@@ -110,5 +111,7 @@ internal fun DocPage.resolveIcon(): ImageVector = when (iconId) {
"measurement" -> MeshtasticIcons.Chart
+ "test-builds" -> MeshtasticIcons.SystemUpdate
+
else -> MeshtasticIcons.Notes
}
diff --git a/obtainium-test-builds.md b/obtainium-test-builds.md
index d14ec8a3de..4ffbced41a 100644
--- a/obtainium-test-builds.md
+++ b/obtainium-test-builds.md
@@ -1,129 +1,10 @@
# Installing Meshtastic Test Builds with Obtainium
-[Obtainium](https://github.com/ImranR98/Obtainium) installs and auto-updates
-Android apps straight from their GitHub releases β no Play Store account, no
-testing-track invite. This guide sets it up for Meshtastic and shows how to
-follow the **open beta** or **closed beta** channels.
+This guide now lives in the documentation site so it gets versioned, published
+per release channel, and bundled into the in-app docs browser:
-> **Heads up β signatures.** Builds from GitHub are signed with the project's
-> release key, **not** Google Play's. If you already have Meshtastic installed
-> from the Play Store, Android will refuse to update over it. You must
-> **uninstall the Play Store version first** (this clears app data), then
-> install via Obtainium and stay on Obtainium for updates. The fdroid and
-> google flavors also differ in signature β pick one and don't switch.
+**β [Developer Guide β Test Builds & Obtainium](docs/en/developer/test-builds.md)**
-## Which channels can Obtainium reach?
-
-Meshtastic for Android promotes builds up a ladder:
-`closed β open β production`. Each maps to a Google Play track, and the GitHub
-release is published (un-drafted) when a build is promoted to closed or higher.
-
-| Channel | Play track | On GitHub | Obtainium can install it? |
-|---|---|---|---|
-| **stable** | Production | published, marked *Latest* | β
Yes |
-| **open** beta | Beta (Open) | published prerelease, tag `vX.Y.Z-open.N` | β
Yes |
-| **closed** beta | Alpha (Closed) | published prerelease, tag `vX.Y.Z-closed.N` | β
Yes |
-| **snapshot** | β (not on Play) | rolling prerelease, tag `snapshot` | β
Yes |
-
-Only **one** promoted test build is "live" on GitHub at a time: as a build is
-promoted, its release object moves forward (its tag changes from `-closed.N` to
-`-open.N` to the clean production tag). So a `-closed`/`-open` build is
-installable only while it is currently parked in that channel β once promoted
-onward, the old channel tag no longer has a release.
-
-**Snapshot** is different: it's an automated debug build of the latest commit on
-`main`, rebuilt and re-published under the single moving `snapshot` tag on every
-push. It never goes to Play. Because debug builds use a `.debug` application-ID
-suffix (`com.geeksville.mesh.fdroid.debug` / `com.geeksville.mesh.google.debug`)
-and the debug signing key, a snapshot installs as its **own separate app** β it
-sits alongside a Play/stable/beta install, so the uninstall-first warning above
-does **not** apply to it.
-
-## Setup
-
-1. Install Obtainium ([GitHub releases](https://github.com/ImranR98/Obtainium/releases)
- or [F-Droid / IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)).
-2. Tap **Add App**.
-3. **App Source URL:**
- ```
- https://github.com/meshtastic/Meshtastic-Android
- ```
-4. Set the options for the channel you want (below).
-5. Tap **Add**, then **Install**.
-
-### Stable
-
-- **Include prereleases:** off
-- *(optional, stricter)* **Verify the 'latest' tag:** on
-- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
-
-### Open beta
-
-- **Include prereleases:** on
-- **Filter release titles by regular expression:** `-open`
-- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
-
-### Closed beta
-
-- **Include prereleases:** on
-- **Filter release titles by regular expression:** `-closed`
-- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
-
-### Bleeding edge (newest promoted test build, any channel)
-
-- **Include prereleases:** on
-- **Filter release titles by regular expression:** `-(closed|open)`
-- **Filter APKs by regular expression:** see [Picking the APK](#picking-the-apk)
-
-Obtainium installs the newest promoted prerelease β whatever is currently in
-open or closed. The title filter is required to skip the always-newer `snapshot`
-prerelease; without it Obtainium would follow snapshot instead.
-
-### Snapshot (latest commit on `main`)
-
-- **Include prereleases:** on
-- **Filter release titles by regular expression:** `^Snapshot`
-- **Filter APKs by regular expression:** debug-signed names, see below
-
-Follows `main` directly β updates on every push. These are **debug builds**
-(`.debug` package, debug key), so they install as a separate app and won't
-disturb a stable/beta install. The APKs are named `β¦-debug-<versionCode>.apk`
-(not `-release.apk`), so use debug-suffixed filters:
-
-| You want | Regex |
-|---|---|
-| Google flavor, most phones (arm64) | `google-arm64-v8a-debug-\d+\.apk` |
-| fdroid flavor, most phones (arm64) | `fdroid-arm64-v8a-debug-\d+\.apk` |
-| fdroid flavor, one-size-fits-all | `fdroid-universal-debug-\d+\.apk` |
-
-Snapshot releases attach only the debug APKs β no `.aab` or desktop installers.
-
-> **If your channel filter finds nothing:** when no build is parked in that
-> exact channel, the title filter matches no current release (old channel tags
-> are orphaned once promoted). For a strict channel pin, turn **Fallback to
-> older releases** *off*. To always have something to install, use the
-> *Bleeding edge* form above instead.
-
-## Picking the APK
-
-Each release attaches the **google** flavor APK, several **fdroid** flavor APKs,
-plus an `.aab` (not installable) and the desktop installers. Pin exactly one
-with **Filter APKs by regular expression**:
-
-| You want | Regex |
-|---|---|
-| **Recommended for testing β Google flavor** | `google-release\.apk` |
-| fdroid flavor, most phones (arm64) | `fdroid-arm64-v8a-release\.apk` |
-| fdroid flavor, one-size-fits-all | `fdroid-universal-release\.apk` |
-
-**Use the Google flavor while testing.** It ships Firebase Crashlytics and
-Datadog RUM, so the crashes and errors you hit get reported back to the team β
-which is the whole point of a test phase. It also has Google push (FCM) and
-Google Maps. Pick an fdroid flavor only if you'd rather not send that
-telemetry. Keep the same flavor on every update β switching flavors triggers
-the signature-mismatch refusal described above.
-
-## Notes
-
-- **Track without installing:** turn on **Track-only** to get update
- notifications without Obtainium downloading anything.
+Prebuilt, distributable Obtainium configurations β an importable export file, the
+one-tap link generator, and the crowdsourced-config-site submission β are in
+[`obtainium/`](obtainium/).
diff --git a/obtainium/README.md b/obtainium/README.md
new file mode 100644
index 0000000000..64b299ea5a
--- /dev/null
+++ b/obtainium/README.md
@@ -0,0 +1,124 @@
+# Obtainium app configurations
+
+Distributable [Obtainium](https://github.com/ImranR98/Obtainium) configurations for
+Meshtastic for Android, written to the format Obtainium and the crowdsourced
+config site expect. Setup instructions and channel reference live in
+[docs/en/developer/test-builds.md](../docs/en/developer/test-builds.md); this
+directory holds the machine-readable artifacts.
+
+| File | What it is |
+|---|---|
+| `com.geeksville.mesh.json` | Submission file for [apps.obtainium.imranr.dev](https://github.com/ImranR98/apps.obtainium.imranr.dev) β stable channel, both flavors |
+| `meshtastic-obtainium-export.json` | Obtainium **Import/Export β Obtainium import** file (stable + snapshot) |
+| `generate-links.py` | Emits `obtainium://app/β¦` one-tap links for every channel |
+
+```bash
+python3 obtainium/generate-links.py
+```
+
+> `com.geeksville.mesh.json` is a **byte-identical mirror** of what we submitted
+> upstream as [apps.obtainium.imranr.dev#1566](https://github.com/ImranR98/apps.obtainium.imranr.dev/pull/1566)
+> (`public/data/apps/complex/com.geeksville.mesh.json`). Change it here and
+> upstream together, or the config site and this repo will disagree. Their
+> formatting conventions are load-bearing for review: 4-space indent, trailing
+> newline, top-level key order `configs, icon, categories, description`, config
+> key order `id, url, author, name, additionalSettings, altLabel` (`altLabel`
+> last), and short lowercase `altLabel`s. Verify with `diff` against their tree
+> rather than by eye. No formatter in this repo touches it β spotless only
+> targets `*.kt` and `*.gradle.kts`.
+
+## The format
+
+An Obtainium app configuration is a flat JSON object. Four keys are required:
+
+| Key | Value |
+|---|---|
+| `id` | Android package name β `com.geeksville.mesh` (debug snapshots: `com.geeksville.mesh.<flavor>.debug`) |
+| `url` | `https://github.com/meshtastic/Meshtastic-Android` β this is what selects the GitHub source |
+| `author` | `meshtastic` |
+| `name` | Display name |
+
+Everything else goes in `additionalSettings`, which is a **JSON string nested
+inside the JSON** β its quotes and backslashes are escaped one extra level. Any
+key you omit takes its default, so only set what you need. The GitHub source
+accepts `includePrereleases`, `fallbackToOlderReleases`,
+`filterReleaseTitlesByRegEx`, `filterReleaseNotesByRegEx`, `verifyLatestTag`,
+`sortMethodChoice`, `useLatestAssetDateAsReleaseDate` and
+`releaseTitleAsVersion`, on top of the source-agnostic keys (`apkFilterRegEx`,
+`versionExtractionRegEx`, `versionDetection`, `autoApkFilterByArch`, `trackOnly`,
+and so on).
+
+Three distribution shapes, all carrying the same object:
+
+- **Config site submission** β `{ "configs": [ β¦, β¦ ], "icon", "categories", "description" }`.
+ A `configs` array (with per-entry `altLabel`) is the "complex" form used when
+ an app needs more than one variant; a single `config` object is the "simple"
+ form. `categories` must come from
+ [`public/data/categories.json`](https://github.com/ImranR98/apps.obtainium.imranr.dev/blob/main/public/data/categories.json)
+ β `messaging` for us.
+- **Import/export file** β `{ "apps": [ β¦ ], "settings": null }`. One entry per
+ package name, because Obtainium keys apps by `id`.
+
+ This is a real constraint on how many channels a user can follow at once.
+ Every non-debug channel β stable `google`, stable `fdroid`, open beta, closed
+ beta, bleeding edge β is `com.geeksville.mesh`, so **only one of them can be
+ tracked at a time**. Adding a second reuses that one entry and switches which
+ channel it follows; it does not create a parallel entry. That is why the
+ export ships stable plus snapshot and not stable plus a beta channel. Only
+ snapshot escapes it, via the `.debug` application-ID suffix
+ (`com.geeksville.mesh.google.debug`), so it can sit alongside a release
+ install.
+- **Deep link** β `obtainium://app/<percent-encoded config JSON>` adds the app
+ with settings baked in. `obtainium://add/<url>` only prefills the Add-App page.
+ Wrapping it as `https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/β¦`
+ keeps the link clickable in places that won't linkify a custom scheme.
+
+## Why our settings are what they are
+
+- **`apkFilterRegEx`** β each release attaches the google-flavor APK, several
+ fdroid-flavor APKs, an `.aab`, and the desktop installers. Obtainium ignores
+ non-APK assets on its own, but the flavors still need disambiguating: both
+ share the `com.geeksville.mesh` package name, so the filter must select
+ exactly one flavor. The fdroid filter deliberately matches *every* ABI split
+ and lets `autoApkFilterByArch` choose, so a new ABI doesn't need a config change.
+- **`autoApkFilterByArch: true` is set explicitly** even though it is the form
+ default. `appJSONCompatibilityModifiers` coerces the key to `false` when it
+ arrives unset, and an unset `preferredApkIndex` becomes `0` β together those
+ would silently pick whichever split happens to be first.
+- **No `versionExtractionRegEx`** β see
+ [Version detection](../docs/en/developer/test-builds.md#version-detection-turns-itself-off--thats-expected).
+ It only rewrites the version parsed from the release, not the installed
+ `versionName` that actually fails to parse, so it cannot fix reconciliation β
+ and it converts a quiet degradation into a hard `NoVersionError` whenever it
+ fails to match.
+- **Snapshot uses date-based pseudo-versioning** β the snapshot tag never moves
+ off the literal string `snapshot`, so there is no version to compare;
+ `releaseDateAsVersion` plus `useLatestAssetDateAsReleaseDate` tracks it by
+ asset date instead. These keys are already in Obtainium's post-migration form,
+ so `_migrateVersionDetectionFormat` passes them through untouched.
+- **`fallbackToOlderReleases` is left at its default (`true`)** on the
+ channel-pinned configs, deliberately. It reads like a strictness knob but
+ isn't: `_selectGitHubTargetRelease` runs
+ `if (!fallbackToOlderReleases && i > prereleaseSkipped) break;`, and
+ `prereleaseSkipped` stays `0` when `includePrereleases` is on β so with
+ fallback off, only the release at index 0 is ever considered and a title
+ filter can never skip past it. Setting it to `false` made the snapshot and
+ beta configs fail outright with `NoReleasesError`. The title filter alone
+ pins the channel.
+
+## Submitting to the config site
+
+[apps.obtainium.imranr.dev](https://apps.obtainium.imranr.dev) is a
+crowdsourced repo that takes pull requests. Meshtastic is not listed there yet.
+
+1. Read [`APP_CRITERIA.md`](https://github.com/ImranR98/apps.obtainium.imranr.dev/blob/main/APP_CRITERIA.md).
+ We qualify: official upstream source, no fork, no reupload mirror.
+2. Copy `com.geeksville.mesh.json` to
+ `public/data/apps/complex/com.geeksville.mesh.json` in a fork of that repo.
+3. `npm i && npm run dev` (Node 22) and confirm the entry renders and its
+ install link works.
+4. Open the PR.
+
+Only the stable configs belong there β the criteria ask for the minimum set of
+variants and for defaults to be left alone, so the beta and snapshot channels
+stay in this repo's own docs.
diff --git a/obtainium/com.geeksville.mesh.json b/obtainium/com.geeksville.mesh.json
new file mode 100644
index 0000000000..f07342a33a
--- /dev/null
+++ b/obtainium/com.geeksville.mesh.json
@@ -0,0 +1,27 @@
+{
+ "configs": [
+ {
+ "id": "com.geeksville.mesh",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic",
+ "additionalSettings": "{\"apkFilterRegEx\":\"google-release\\\\.apk$\"}",
+ "altLabel": "google (FCM, Maps)"
+ },
+ {
+ "id": "com.geeksville.mesh",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic",
+ "additionalSettings": "{\"apkFilterRegEx\":\"fdroid-.*-release\\\\.apk$\",\"autoApkFilterByArch\":true}",
+ "altLabel": "fdroid (no Google services)"
+ }
+ ],
+ "icon": "https://github.com/meshtastic/Meshtastic-Android/raw/main/fastlane/metadata/android/en-US/images/icon.png",
+ "categories": [
+ "messaging"
+ ],
+ "description": {
+ "en": "The official app for Meshtastic, an open-source, off-grid, mesh radio."
+ }
+}
diff --git a/obtainium/generate-links.py b/obtainium/generate-links.py
new file mode 100644
index 0000000000..de8e390efe
--- /dev/null
+++ b/obtainium/generate-links.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+"""Emit Obtainium one-tap install links for the Meshtastic channel configs.
+
+Obtainium's `obtainium://app/<url-encoded config JSON>` scheme adds an app with
+its settings baked in. The apps.obtainium.imranr.dev redirect wrapper exists so
+the link survives being pasted somewhere that won't linkify a custom scheme.
+
+Usage:
+ python3 obtainium/generate-links.py # markdown table
+ python3 obtainium/generate-links.py --json # raw config JSON per channel
+"""
+
+import argparse
+import json
+import sys
+from urllib.parse import quote
+
+REPO_URL = "https://github.com/meshtastic/Meshtastic-Android"
+REDIRECT = "https://apps.obtainium.imranr.dev/redirect.html?r="
+
+# Release assets are named androidApp-<flavor>[-<abi>]-release.apk; snapshot
+# builds attach androidApp-<flavor>-<abi>-debug-<versionCode>.apk instead.
+GOOGLE_APK = r"google-release\.apk$"
+FDROID_APK = r"fdroid-.*-release\.apk$"
+SNAPSHOT_APK = r"google-.*-debug-\d+\.apk$"
+
+CHANNELS = [
+ (
+ "Stable (Google flavor)",
+ "com.geeksville.mesh",
+ "Meshtastic",
+ {"apkFilterRegEx": GOOGLE_APK},
+ ),
+ (
+ "Stable (F-Droid flavor)",
+ "com.geeksville.mesh",
+ "Meshtastic",
+ {"apkFilterRegEx": FDROID_APK, "autoApkFilterByArch": True},
+ ),
+ (
+ "Open beta",
+ "com.geeksville.mesh",
+ "Meshtastic Beta",
+ {
+ "includePrereleases": True,
+ "filterReleaseTitlesByRegEx": "-open",
+ "apkFilterRegEx": GOOGLE_APK,
+ },
+ ),
+ (
+ "Closed beta",
+ "com.geeksville.mesh",
+ "Meshtastic Alpha",
+ {
+ "includePrereleases": True,
+ "filterReleaseTitlesByRegEx": "-closed",
+ "apkFilterRegEx": GOOGLE_APK,
+ },
+ ),
+ (
+ "Bleeding edge (newest promoted test build)",
+ "com.geeksville.mesh",
+ "Meshtastic Beta",
+ {
+ "includePrereleases": True,
+ "filterReleaseTitlesByRegEx": "-(closed|open)",
+ "apkFilterRegEx": GOOGLE_APK,
+ },
+ ),
+ (
+ "Snapshot (latest commit on main)",
+ "com.geeksville.mesh.google.debug",
+ "Meshtastic Snapshot",
+ {
+ "includePrereleases": True,
+ "filterReleaseTitlesByRegEx": "^Snapshot",
+ "useLatestAssetDateAsReleaseDate": True,
+ # The tag never moves off "snapshot", so date is the only usable version.
+ "versionDetection": False,
+ "releaseDateAsVersion": True,
+ "apkFilterRegEx": SNAPSHOT_APK,
+ "autoApkFilterByArch": True,
+ },
+ ),
+]
+
+
+def config_for(app_id, name, settings):
+ return {
+ "id": app_id,
+ "url": REPO_URL,
+ "author": "meshtastic",
+ "name": name,
+ "additionalSettings": json.dumps(settings, separators=(",", ":")),
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--json", action="store_true", help="print configs, not links")
+ args = parser.parse_args()
+
+ if args.json:
+ for label, app_id, name, settings in CHANNELS:
+ print(f"# {label}")
+ print(json.dumps(config_for(app_id, name, settings), separators=(",", ":")))
+ return 0
+
+ print("| Channel | Link |")
+ print("|---|---|")
+ for label, app_id, name, settings in CHANNELS:
+ blob = json.dumps(config_for(app_id, name, settings), separators=(",", ":"))
+ # Only the JSON payload is percent-encoded; the scheme prefix stays literal.
+ scheme = "obtainium://app/" + quote(blob, safe="")
+ print(f"| {label} | [Add to Obtainium]({REDIRECT}{scheme}) |")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/obtainium/meshtastic-obtainium-export.json b/obtainium/meshtastic-obtainium-export.json
new file mode 100644
index 0000000000..1a402b4e4b
--- /dev/null
+++ b/obtainium/meshtastic-obtainium-export.json
@@ -0,0 +1,19 @@
+{
+ "apps": [
+ {
+ "id": "com.geeksville.mesh",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic",
+ "additionalSettings": "{\"apkFilterRegEx\":\"google-release\\\\.apk$\"}"
+ },
+ {
+ "id": "com.geeksville.mesh.google.debug",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic Snapshot",
+ "additionalSettings": "{\"includePrereleases\":true,\"filterReleaseTitlesByRegEx\":\"^Snapshot\",\"useLatestAssetDateAsReleaseDate\":true,\"versionDetection\":false,\"releaseDateAsVersion\":true,\"apkFilterRegEx\":\"google-.*-debug-\\\\d+\\\\.apk$\",\"autoApkFilterByArch\":true}"
+ }
+ ],
+ "settings": null
+}
Served by rngit 1.5.2 - Generated in 0.2s